Create resource_extra_fields_export.py#47
Conversation
Python script to export extra fields of items in a specific resource category
WalkthroughA new example script was added to export items from an eLabFTW category to CSV and XLSX. It configures an API client, fetches items, normalizes extra_fields according to a fixed FIELD_ORDER, and writes UTF-8 CSV and formatted Excel outputs. Changes
Sequence Diagram(s)sequenceDiagram
participant Script as "Export Script"
participant ApiClient as "elabapi_python.ApiClient"
participant eLabAPI as "eLabFTW API"
participant FS as "Filesystem (CSV/XLSX)"
Script->>ApiClient: configure (API_HOST_URL, API_KEY)
Script->>ApiClient: request items for CATEGORY_ID
ApiClient->>eLabAPI: GET /items?category=CATEGORY_ID
eLabAPI-->>ApiClient: return items JSON
ApiClient-->>Script: deliver items
Script->>Script: parse items, normalize extra_fields, apply FIELD_ORDER
Script->>FS: write UTF-8 CSV (headers + rows)
Script->>FS: create XLSX, write headers (bold), rows, autofilter, autosize
Script-->>FS: close files
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
examples/resource_extra_fields_export.py (2)
121-131: Recommend wrapping execution in a main guard, Ensign.Running script logic at module level prevents reuse and testability. The Federation standard is to use
if __name__ == "__main__":to encapsulate execution logic.♻️ Proposed refactor: Add main guard
+def main(): + print("Starting export...") + + # Retrieve all items from the specified category + try: + items = itemsApi.read_items(cat=CATEGORY_ID) + except Exception as e: + print(f"Failed to retrieve items: {e}") + return + + print(f"Items found: {len(items)}") + # ... rest of logic ... + +if __name__ == "__main__": + main()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/resource_extra_fields_export.py` around lines 121 - 131, The script runs top-level logic (prints and calls itemsApi.read_items with CATEGORY_ID) which prevents reuse and testing; move the execution into a main function (e.g., def main(): containing the print("Starting export..."), items = itemsApi.read_items(cat=CATEGORY_ID), and print(f"Items found: {len(items)}")) and then add the standard guard if __name__ == "__main__": main() so the module can be imported without side effects.
173-182: A minor course correction for modern Python idioms.The static analysis correctly notes that spread syntax is more elegant than concatenation. A small improvement, but precision matters on the bridge.
✨ Proposed refactor: Use spread operator
# Define final column structure -csv_columns = ["Ressourcen ID", "Titel"] + FIELD_ORDER +csv_columns = ["Ressourcen ID", "Titel", *FIELD_ORDER]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/resource_extra_fields_export.py` around lines 173 - 182, Replace the list concatenation used to build csv_columns with Python's list unpacking (spread) to produce a cleaner expression: update the csv_columns construction that currently uses ["Ressourcen ID", "Titel"] + FIELD_ORDER to use ["Ressourcen ID", "Titel", *FIELD_ORDER]; keep the rest of the CSV writing logic (with open(OUTPUT_FILE, ...), csv.DictWriter, writer.writeheader(), writer.writerows(rows)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 161-165: The code that converts list-type fields into a
comma-separated string should coerce each element to a string before joining to
avoid TypeError for non-string items; update the block that checks
isinstance(value, list) (using the variables value, field, row) to build a list
of strings (e.g., [str(v) for v in value]) and then join that list, so
row[field] gets a safe string representation.
- Around line 145-152: Wrap the json.loads call that parses item.metadata in a
try/except to catch json.JSONDecodeError (and optionally ValueError), so
malformed JSON doesn't crash the export; on error set extra_fields = {} (and/or
metadata = {}) and log or warn about the bad payload referencing item.metadata
or item.id for debugging. Specifically update the block that assigns metadata =
json.loads(metadata_raw) / extra_fields = metadata.get("extra_fields", {}) to
handle parsing failures and fall back to an empty dict for extra_fields.
- Around line 212-220: The loop over "for cell in col" silently swallows all
exceptions via a bare except; change it to catch specific exceptions (e.g.,
TypeError, ValueError, AttributeError) when calling len(str(cell.value)) and
handle them explicitly—either skip the cell or record/log the error—so
max_length is still computed safely; update the block around the "for cell in
col" loop and the calculation of adjusted_width and
ws.column_dimensions[column_letter].width to use the specific exception types
and emit a warning (or use the module logger) when an unexpected cell value is
encountered.
- Around line 36-47: The example hardcodes sensitive configuration
(API_HOST_URL, API_KEY) and other settings (CATEGORY_ID, OUTPUT_FILE,
OUTPUT_XLSX); replace those constants with reads from environment variables
(e.g., os.environ.get or dotenv) and add explicit validation/fallbacks—require
API_KEY and API_HOST_URL to be set and raise a clear error if missing, and allow
CATEGORY_ID, OUTPUT_FILE, OUTPUT_XLSX to default from env vars if present;
update any top-level comments to instruct users to set env vars rather than
paste keys into the file.
---
Nitpick comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 121-131: The script runs top-level logic (prints and calls
itemsApi.read_items with CATEGORY_ID) which prevents reuse and testing; move the
execution into a main function (e.g., def main(): containing the print("Starting
export..."), items = itemsApi.read_items(cat=CATEGORY_ID), and print(f"Items
found: {len(items)}")) and then add the standard guard if __name__ ==
"__main__": main() so the module can be imported without side effects.
- Around line 173-182: Replace the list concatenation used to build csv_columns
with Python's list unpacking (spread) to produce a cleaner expression: update
the csv_columns construction that currently uses ["Ressourcen ID", "Titel"] +
FIELD_ORDER to use ["Ressourcen ID", "Titel", *FIELD_ORDER]; keep the rest of
the CSV writing logic (with open(OUTPUT_FILE, ...), csv.DictWriter,
writer.writeheader(), writer.writerows(rows)) unchanged.
| # Metadata is stored as JSON string | ||
| metadata_raw = item.metadata | ||
|
|
||
| if metadata_raw: | ||
| metadata = json.loads(metadata_raw) | ||
| extra_fields = metadata.get("extra_fields", {}) | ||
| else: | ||
| extra_fields = {} |
There was a problem hiding this comment.
We must anticipate malformed data, Number One.
The json.loads call will raise a JSONDecodeError if the metadata contains invalid JSON. In space—and in data processing—we prepare for the unexpected.
🛡️ Proposed fix: Handle JSON parsing errors
# Metadata is stored as JSON string
metadata_raw = item.metadata
if metadata_raw:
- metadata = json.loads(metadata_raw)
- extra_fields = metadata.get("extra_fields", {})
+ try:
+ metadata = json.loads(metadata_raw)
+ extra_fields = metadata.get("extra_fields", {})
+ except json.JSONDecodeError:
+ print(f"Warning: Invalid metadata JSON for item {item.id}")
+ extra_fields = {}
else:
extra_fields = {}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/resource_extra_fields_export.py` around lines 145 - 152, Wrap the
json.loads call that parses item.metadata in a try/except to catch
json.JSONDecodeError (and optionally ValueError), so malformed JSON doesn't
crash the export; on error set extra_fields = {} (and/or metadata = {}) and log
or warn about the bad payload referencing item.metadata or item.id for
debugging. Specifically update the block that assigns metadata =
json.loads(metadata_raw) / extra_fields = metadata.get("extra_fields", {}) to
handle parsing failures and fall back to an empty dict for extra_fields.
| # Convert list-type fields into comma-separated string | ||
| if isinstance(value, list): | ||
| value = ", ".join(value) | ||
|
|
||
| row[field] = value |
There was a problem hiding this comment.
Ensure type safety before joining list values.
If value contains non-string elements, the join operation will fail. A minor adjustment ensures we navigate safely through any data anomalies.
🛡️ Proposed fix: Convert elements to strings
# Convert list-type fields into comma-separated string
if isinstance(value, list):
- value = ", ".join(value)
+ value = ", ".join(str(v) for v in value)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/resource_extra_fields_export.py` around lines 161 - 165, The code
that converts list-type fields into a comma-separated string should coerce each
element to a string before joining to avoid TypeError for non-string items;
update the block that checks isinstance(value, list) (using the variables value,
field, row) to build a list of strings (e.g., [str(v) for v in value]) and then
join that list, so row[field] gets a safe string representation.
| for cell in col: | ||
| try: | ||
| if cell.value: | ||
| max_length = max(max_length, len(str(cell.value))) | ||
| except: | ||
| pass | ||
|
|
||
| adjusted_width = max_length + 2 | ||
| ws.column_dimensions[column_letter].width = adjusted_width |
There was a problem hiding this comment.
A bare except clause is unworthy of Starfleet protocols.
This silently swallows all exceptions, making debugging nearly impossible when anomalies occur. We must be specific about what we catch, or at minimum, log the encounter.
🛡️ Proposed fix: Handle exceptions properly
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
- except:
- pass
+ except (TypeError, AttributeError):
+ continue📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for cell in col: | |
| try: | |
| if cell.value: | |
| max_length = max(max_length, len(str(cell.value))) | |
| except: | |
| pass | |
| adjusted_width = max_length + 2 | |
| ws.column_dimensions[column_letter].width = adjusted_width | |
| for cell in col: | |
| try: | |
| if cell.value: | |
| max_length = max(max_length, len(str(cell.value))) | |
| except (TypeError, AttributeError): | |
| continue | |
| adjusted_width = max_length + 2 | |
| ws.column_dimensions[column_letter].width = adjusted_width |
🧰 Tools
🪛 Ruff (0.15.2)
[error] 216-216: Do not use bare except
(E722)
[error] 216-217: try-except-pass detected, consider logging the exception
(S110)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/resource_extra_fields_export.py` around lines 212 - 220, The loop
over "for cell in col" silently swallows all exceptions via a bare except;
change it to catch specific exceptions (e.g., TypeError, ValueError,
AttributeError) when calling len(str(cell.value)) and handle them
explicitly—either skip the cell or record/log the error—so max_length is still
computed safely; update the block around the "for cell in col" loop and the
calculation of adjusted_width and ws.column_dimensions[column_letter].width to
use the specific exception types and emit a warning (or use the module logger)
when an unexpected cell value is encountered.
MoustaphaCamara
left a comment
There was a problem hiding this comment.
suggestion: add to .gitignore:
diff --git a/.gitignore b/.gitignore
index 0ada496..31fb00d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ openapi.yaml
html
venv
__pycache__
+export.*
diff --git a/examples/resource_extra_fields_export.py b/examples/resource_extra_fields_export.py
index c91031b..0f8c553 100644
--- a/examples/resource_extra_fields_export.py
+++ b/examples/resource_extra_fields_export.py
@@ -152,9 +152,12 @@ for item in items:
extra_fields = {}
# Extract all fields defined in FIELD_ORDER
+ normalized_fields = {k.strip().lower(): v for k, v in extra_fields.items()}
+
for field in FIELD_ORDER:
- if field in extra_fields:
- value = extra_fields[field].get("value", "")
+ key = field.strip().lower()
+ if key in normalized_fields:
+ value = normalized_fields[key].get("value", "")
else:
value = ""
|
Thanks in advance for this addition! We'll make sure to merge it as soon as it's corrected :) |
|
I made a pull request in your fork so you can cherry pick my changes and keep what you need 👍 |
Co-authored-by: Mouss <104678778+MoustaphaCamara@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
examples/resource_extra_fields_export.py (2)
53-74: Number One, we appear to be reinventing the warp drive.The
examples/client.pymodule already provides a well-structured, reusable API client configuration that handles proxy settings, CA certificates, and debug flags. Duplicating this logic here creates maintenance burden and inconsistencies.Additionally, the API key is configured twice: via
configuration.api_key(lines 55-56) and viaset_default_header(lines 68-71). Theclient.pypattern uses onlyset_default_header, which is the authoritative method for this library.♻️ Proposed refactor: Reuse the existing client module
-import elabapi_python import json import csv +import os from openpyxl import Workbook from openpyxl.styles import Font from openpyxl.utils import get_column_letter +# Import the shared API client configuration +from client import api_client +import elabapi_python ######################### # CONFIG ######################### -# Base URL of your eLabFTW API (must include /api/v2) -API_HOST_URL = 'https://YOUR-URL/api/v2' - -# Personal API key generated in eLabFTW -API_KEY = 'YOUR API Key' - # Category ID from which items should be exported -CATEGORY_ID = 123456 +CATEGORY_ID = int(os.environ.get('ELAB_CATEGORY_ID', '123456')) # Output filenames -OUTPUT_FILE = "export.csv" -OUTPUT_XLSX = "export.xlsx" - -######################### -# API CONFIGURATION -######################### - -# Create configuration object for the eLabFTW API -configuration = elabapi_python.Configuration() -configuration.api_key['api_key'] = API_KEY -configuration.api_key_prefix['api_key'] = 'Authorization' -configuration.host = API_HOST_URL - -configuration.debug = False - -# Set to True if valid SSL certificates are used -configuration.verify_ssl = True - -# Create API client -api_client = elabapi_python.ApiClient(configuration) - -# Set authorization header manually (required for authentication) -api_client.set_default_header( - header_name='Authorization', - header_value=API_KEY -) +OUTPUT_FILE = os.environ.get('EXPORT_CSV', 'export.csv') +OUTPUT_XLSX = os.environ.get('EXPORT_XLSX', 'export.xlsx') # Load Items API endpoint itemsApi = elabapi_python.ItemsApi(api_client)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/resource_extra_fields_export.py` around lines 53 - 74, The code duplicates client setup and sets the API key twice; instead import and reuse the shared client builder from examples.client (e.g., the factory function that returns an ApiClient) rather than instantiating elabapi_python.Configuration/ApiClient directly, remove the configuration.api_key[...] assignment and keep only the authoritative set_default_header usage, and then instantiate ItemsApi with the reused ApiClient (look for symbols like Configuration, api_key, set_default_header, ApiClient, ItemsApi and the shared factory in examples.client) so proxy/CA/debug logic is centralized.
176-176: A minor course correction, as recommended by our static analysis sensors.The linter suggests using spread syntax for list concatenation, which is more idiomatic in modern Python.
♻️ Proposed fix
-csv_columns = ["Ressourcen ID", "Titel"] + FIELD_ORDER +csv_columns = ["Ressourcen ID", "Titel", *FIELD_ORDER]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/resource_extra_fields_export.py` at line 176, Replace the list concatenation that builds csv_columns using the + operator with Python's list unpacking: construct a new list literal containing the two initial strings ("Ressourcen ID" and "Titel") and expand FIELD_ORDER into it using the star-unpacking operator; update the assignment to csv_columns to use this pattern so csv_columns becomes a single list literal with the items from FIELD_ORDER unpacked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 125-131: Wrap the script execution in a main guard (if __name__ ==
"__main__":) so importing this module won't run the export immediately, and add
defensive error handling around the itemsApi.read_items call: call
itemsApi.read_items(cat=CATEGORY_ID) inside a try/except that catches broad
request/API exceptions (and Exception as a fallback), logs/prints a clear
diagnostic including the exception message and returns/exit non‑zero on failure;
ensure variables referenced after the call (like items and len(items)) are only
used when the call succeeds. Reference: itemsApi.read_items and the top-level
script body where the print("Starting export...") and items =
itemsApi.read_items(...) occur.
---
Nitpick comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 53-74: The code duplicates client setup and sets the API key
twice; instead import and reuse the shared client builder from examples.client
(e.g., the factory function that returns an ApiClient) rather than instantiating
elabapi_python.Configuration/ApiClient directly, remove the
configuration.api_key[...] assignment and keep only the authoritative
set_default_header usage, and then instantiate ItemsApi with the reused
ApiClient (look for symbols like Configuration, api_key, set_default_header,
ApiClient, ItemsApi and the shared factory in examples.client) so proxy/CA/debug
logic is centralized.
- Line 176: Replace the list concatenation that builds csv_columns using the +
operator with Python's list unpacking: construct a new list literal containing
the two initial strings ("Ressourcen ID" and "Titel") and expand FIELD_ORDER
into it using the star-unpacking operator; update the assignment to csv_columns
to use this pattern so csv_columns becomes a single list literal with the items
from FIELD_ORDER unpacked.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: db666e3f-4259-40b2-ae9a-9ef784de1937
📒 Files selected for processing (1)
examples/resource_extra_fields_export.py
| print("Starting export...") | ||
|
|
||
| # Retrieve all items from the specified category | ||
| items = itemsApi.read_items(cat=CATEGORY_ID) | ||
|
|
||
| print(f"Items found: {len(items)}") | ||
|
|
There was a problem hiding this comment.
Engage defensive protocols: the API call lacks error handling.
The read_items call on line 128 can fail due to network issues, authentication errors, or invalid category IDs. Currently, any exception would crash the script without a clear diagnostic message. Additionally, this script lacks a if __name__ == "__main__": guard, meaning it executes immediately upon import—problematic if someone attempts to reuse functions from this module.
🛡️ Proposed fix: Add main guard and API error handling
+def main():
+ print("Starting export...")
-print("Starting export...")
+ # Retrieve all items from the specified category
+ try:
+ items = itemsApi.read_items(cat=CATEGORY_ID)
+ except elabapi_python.ApiException as e:
+ print(f"API request failed: {e}")
+ return
-# Retrieve all items from the specified category
-items = itemsApi.read_items(cat=CATEGORY_ID)
+ print(f"Items found: {len(items)}")
+ # ... rest of logic ...
-print(f"Items found: {len(items)}")
+if __name__ == "__main__":
+ main()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/resource_extra_fields_export.py` around lines 125 - 131, Wrap the
script execution in a main guard (if __name__ == "__main__":) so importing this
module won't run the export immediately, and add defensive error handling around
the itemsApi.read_items call: call itemsApi.read_items(cat=CATEGORY_ID) inside a
try/except that catches broad request/API exceptions (and Exception as a
fallback), logs/prints a clear diagnostic including the exception message and
returns/exit non‑zero on failure; ensure variables referenced after the call
(like items and len(items)) are only used when the call succeeds. Reference:
itemsApi.read_items and the top-level script body where the print("Starting
export...") and items = itemsApi.read_items(...) occur.
Python script to export extra fields of items in a specific resource category.
Summary by CodeRabbit